/*
 *  This file is part of Bracket Properties
 *  Copyright 2011-2013 David R. Smith
 *  All Rights Reserved
 *
 */
package asia.redact.bracket.properties.serializer;

import java.io.IOException;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import net.iharder.Base64;

import asia.redact.bracket.properties.Properties;

/**
 * This class assembles objects serialized to a properties file into a java object graph.
 * 
 * @author Dave
 *
 */
public class Assembler {
	
	private Object result;
	final private Properties props;
	final private PackageMap packages;
	
	Pattern OBJECT_PARAM_PAT = Pattern.compile("(\\d+)\\.([a-z][a-zA-Z0-9]+)");
	
	private Lock lock = new ReentrantLock();
	private Logger log = Logger.getLogger(Assembler.class);

	public Assembler(Properties props) {
		this.props = props;
		packages = new PackageMap();
		packages.loadFromProperties(props);
		log.debug("loaded "+packages.size()+" package names");
	}
	
	public void assemble() {
		lock.lock();
		try{	
			
			boolean ok = false;
			if(props.containsKey(Renderer.INITIAL_KEY_NAME) && props.containsKey(Renderer.NEXT_KEY_NAME)){
				ok = true;
			}
			
			if(!ok) throw new RuntimeException("Missing either "+Renderer.INITIAL_KEY_NAME+" or "+Renderer.NEXT_KEY_NAME);
			
			final long initialIndex = Long.valueOf(props.get(Renderer.INITIAL_KEY_NAME));
			// final long lastIndex = Long.valueOf(props.get(Renderer.NEXT_KEY_NAME))-1;
			
			result = assemble(initialIndex, 0);
		
		}finally{
			lock.unlock();
		}
	
	}
	
	/**
	 * For custom starting place
	 * 
	 * @param rootKey
	 */
	public void assemble(String rootKey) {
		lock.lock();
		try{	
			final long initialIndex = Long.valueOf(props.get(rootKey));
			result = assemble(initialIndex, 0);
		}finally{
			lock.unlock();
		}
	
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private Object assemble(long currentIndex, int depth){
		lock.lock();
		try{	
			
			depth++;
		
			final String sIndex = String.valueOf(currentIndex);
			final String rawValue = props.get(sIndex);
			final String [] args = rawValue.split("\\|");
			if(args == null || args.length < 2) throw new RuntimeException("Problem, token looks bad: "+sIndex);
			TokenType tt = TokenType.valueOf(args[0]);
			String value =  args[1];
		
			switch(tt){
				case LOADABLE: {
					Class<?> cls;
					try {
						cls = Class.forName(packages.classNameFor(value));
						Object obj = cls.newInstance();
						String rep = props.get(sIndex+".fromString");
						((Loadable)obj).fromString(rep);
						return obj;
					} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
						e.printStackTrace();
					}
					
				}
				case N: { return null; }
				case B: { return Boolean.valueOf(value); }
				case P: { return String.valueOf(value); } //TODO is this token ever called?
				case S: { return value; }
				case I: { return Integer.valueOf(value); }
				case LO: { return Long.valueOf(value); }
				case F: { return Float.valueOf(value); }
				case D: { return Double.valueOf(value); }
				case O: {
					try {
						Class<?> cls = Class.forName(packages.classNameFor(value));
						Object obj = cls.newInstance();
					
						//subtle bug in this
						//PropertiesImpl impl = (PropertiesImpl) props;
						//List<String> keys = impl.getKeyGroup(sIndex);
						
						List<String> keys = new ArrayList<String>();
						for(String s: props.getPropertyMap().keySet()){
							if(s.indexOf(sIndex+".") == 0){ //need to terminate the token
								keys.add(s);
							}
						}
						
				
						for(String s: keys){
				
							Matcher matcher = OBJECT_PARAM_PAT.matcher(s);
							if(matcher.matches()){
					
								String part = matcher.group(2);
								String val = props.get(s);
						
								if(val != null && val.startsWith("$")) {
							//		print(depth, "$$ we match for indirect $$");
									long number = Long.valueOf(val.substring(1,val.length()));
								//	print(depth, "val = "+val+", number = "+number);
									// recurse 
								//	print(depth, "Recursing into assemble using "+number);
									Object res = assemble(number, depth);
								//	print(depth, "$$ creating setter to set "+res+" $$");
									new MethodSetter(cls,obj,part,null).set(res);
								}else{
								//	print(depth, "$$ we default to direct:"+cls+","+part+","+val+" $$");
									new MethodSetter(cls,obj,part,val).set();
								}
							}else{
							//	print(depth, "$$$ did not match $$$");
							}
						}
						
						return obj;
						
					} catch (ClassNotFoundException e) {
						e.printStackTrace();
					} catch (InstantiationException e) {
						e.printStackTrace();
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					}
					break;
				}
				case LI: {
					// might be a third arg, this is the inferred list type based on the first argument
					// it is the token NONE if the list is empty
					Class<?> argClass = String.class;
					if(args.length == 3 && (!args[2].equals("NONE"))){
						try {
							argClass = Class.forName(packages.classNameFor(args[2]));
						} catch (ClassNotFoundException e) {
							e.printStackTrace();
						}
					}
					
					try {
						Class<?> cls = Class.forName(packages.classNameFor(value));
						List list =  (List) cls.newInstance();
						List<String> keys = props.getListKeys(sIndex);
						Object item = null;
						for(String key: keys) {
							String val = props.get(key);
							if(val != null && val.startsWith("$")) {
								Long number = Long.valueOf(val.substring(1,val.length()));
								item = assemble(number,depth);
							}else{
								
								//TODO this needs to tie back to the RenderImmediate TokenTypes
								if(argClass == Integer.class){
									item = Integer.valueOf(val);
								}else if(argClass == Long.class){
									item = Long.valueOf(val);
								}else if(argClass == Double.class){
									item = Double.valueOf(val);
								}else if(argClass == Float.class){
									item = Float.valueOf(val);
								}else if(argClass == Short.class){
									item = Short.valueOf(val);
								}else if(argClass == Boolean.class){
									item = Boolean.valueOf(val);
								}else if(argClass == null){
									item = null;
								}else if(argClass == Long.class){
									item = Long.valueOf(val);
								}else if(argClass == String.class){
									item = val;
								}
							}
							list.add(item);
						}
						
						return list;
						
					} catch (ClassNotFoundException e) {
						e.printStackTrace();
					} catch (InstantiationException e) {
						e.printStackTrace();
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					}
					
					break;
				}
				case M: {
					try {
						
						// might be a third and fourth arg, these are the inferred Map key/value types based on the arguments
						// it is the token NONE if the Map is empty
						Class<?> keyClass = String.class;
						Class<?> valueClass = String.class;
						if(args.length == 4 && (!args[2].equals("NONE"))){
							try {
								keyClass = Class.forName(packages.classNameFor(args[2]));
							} catch (ClassNotFoundException e) {
								e.printStackTrace();
							}
						}
						
						if(args.length == 4 && (!args[3].equals("NONE"))){
							try {
								valueClass = Class.forName(packages.classNameFor(args[3]));
							} catch (ClassNotFoundException e) {
								e.printStackTrace();
							}
						}	
						
						
					Class<?> cls = Class.forName(packages.classNameFor(value));
					Map mapobj =  (Map) cls.newInstance();
					List<String> keys = props.getMapKeys(sIndex); //they should be in order
					int length = keys.size();
					for(int i = 0; i<length;i+=2){
						String keyskey = keys.get(i);
						String keysValue = props.get(keyskey);
						String valueskey = keys.get(i+1);
						String valuesValue = props.get(valueskey);
						Object kobj=null;
						Object vobj=null;
						if(keysValue != null && keysValue.startsWith("$")) {
							Long number = Long.valueOf(keysValue.substring(1,keysValue.length()));
							kobj = assemble(number,depth);
						}else{
							if(keyClass==Integer.class){
								kobj=Integer.valueOf(keysValue);
							}else if(keyClass==Long.class){
								kobj=Long.valueOf(keysValue);
							}else if(keyClass==Double.class){
								kobj=Double.valueOf(keysValue);
							}else if(keyClass==Float.class){
								kobj=Float.valueOf(keysValue);
							}else if(keyClass==Short.class){
								kobj=Short.valueOf(keysValue);
							}else if(keyClass==Boolean.class){
								kobj=Boolean.valueOf(keysValue);
							}else{
								kobj=keysValue;
							}
						}
						if(valuesValue != null && valuesValue.startsWith("$")) {
							Long number = Long.valueOf(valuesValue.substring(1,valuesValue.length()));
							vobj = assemble(number,depth);
						}else{
							if(valueClass==Integer.class){
								vobj=Integer.valueOf(valuesValue);
							}else if(valueClass==Long.class){
								vobj=Long.valueOf(valuesValue);
							}else if(valueClass==Double.class){
								vobj=Double.valueOf(valuesValue);
							}else if(valueClass==Float.class){
								vobj=Float.valueOf(valuesValue);
							}else if(valueClass==Short.class){
								vobj=Short.valueOf(valuesValue);
							}else if(valueClass==Boolean.class){
								vobj=Boolean.valueOf(valuesValue);
							}else{
								vobj=valuesValue;
							}
						}
						
						mapobj.put(kobj, vobj);
						
					}
					
					return mapobj;
					
					} catch (ClassNotFoundException e) {
						e.printStackTrace();
					} catch (InstantiationException e) {
						e.printStackTrace();
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					}
					
					break;
				}
				case A: {
					
					long arrayLength = 0;
					if(args.length == 3){
						arrayLength = Long.valueOf(args[2]);
					}
					
					if(value.startsWith("[L")){
						try {
						List<String> keys = props.getListKeys(sIndex);
						Class<?> c = Class.forName(packages.classNameFor(value.substring(2,value.length()-1)));
						Object array = Array.newInstance(c, (int) arrayLength);
						int index = 0;
						for(String key: keys){
							//Array.set(array,index,obj);
							String childVal = props.get(key);
							if(childVal != null && childVal.startsWith("$")) {
								Long number = Long.valueOf(childVal.substring(1,childVal.length()));
								Object o = assemble(number,depth);
								Array.set(array, index, o);
							}
							index++;
						}
						return array;
					  }catch(Exception x) {
						x.printStackTrace();
					  }
					}else if(value.equals("[C")){
						String s = props.get(sIndex+".string");
						return s.toCharArray();
					}else{
							// all the rest imply a byte array has been serialized on the sender side
						
							String s = props.get(sIndex+".base64");
							byte [] bytes = null;
							try {
								bytes = Base64.decode(s);
							} catch (IOException e) {
								e.printStackTrace();
							}
							switch(value.substring(0,2)){
								case "[B": {
									return bytes;
								}
								case "[I": {
									 ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);  
									 byteBuffer.put(bytes);
									 int[] ints = new int[(int)(bytes.length/4)];
									 int count = 0;
									 for(int i=0;i<bytes.length;i+=4){
										 ints[count]=byteBuffer.getInt(i);
										 count++;
									 }
									 // doesn't work, apparently this an optional method
								    // IntBuffer intBuffer = byteBuffer.asIntBuffer();
								    // return intBuffer.array();
									 
								     return ints;
								}
								case "[F": {
									//float
									 ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);  
									 byteBuffer.put(bytes);
									 float [] floats = new float[(int)(bytes.length/4)];
									 int count = 0;
									 for(int i=0;i<bytes.length;i+=4){
										 floats[count]=byteBuffer.getFloat(i);
										 count++;
									 }
									 return floats;
								}
								case "[J": {
									//long
									 ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);  
									 byteBuffer.put(bytes);
									 long[] longs = new long[(int)(bytes.length/8)];
									 int count = 0;
									 for(int i=0;i<bytes.length;i+=8){
										 longs[count]=byteBuffer.getLong(i);
										 count++;
									 }
									 return longs;
								}
								case "[D": {
									//double
									 ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); 
									 byteBuffer.put(bytes);
									 double[] doubles = new double[(int)(bytes.length/8)];
									 int count = 0;
									 for(int i=0;i<bytes.length;i+=8){
										 doubles[count]=byteBuffer.getDouble(i);
										 count++;
									 }
									 return doubles;
								}
								case "[Z": {
									//boolean
									// TODO
								}
								case "[S": {
									//short
									 ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); 
									 byteBuffer.put(bytes);
									 short[] shorts = new short[(int)bytes.length/2];
									 int count=0;
									 for(int i=0;i<bytes.length;i+=2){
										 shorts[count]=byteBuffer.getShort(i);
										 count++;
									 }
									 return shorts;
								}
								default: {
									break;
								}
							}
					}
		     
					break;
				}
				case E: {
					try {
						String name = props.get(sIndex+".name"); //enum name
						Class<?> cls = Class.forName(packages.classNameFor(value));
						List list = Arrays.asList(cls.getEnumConstants());
						Object targetEnumConst = null;
						for(Object obj: list){
							if(String.valueOf(obj).equals(name)){
								targetEnumConst = obj;
								break;
							}
						}
						
						return targetEnumConst;
					} catch (ClassNotFoundException e) {
						e.printStackTrace();
					}
					break;
				}
			default:
				break;
			}
		
		}finally{
			lock.unlock();
		}
		
		return null;
	}
	
	/**
	 * For debugging purposes - internal only
	 * @param depth
	 * @param msg
	 */
	void print(int depth, String msg) {
		StringBuffer buf = new StringBuffer();
		for(int i = 0; i<depth;i++) buf.append(" ");
		log.debug(buf.toString()+" "+msg);
	}

	public Object getResult() {
		return result;
	}

}


